> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/jaypopat/cf_ai_duet/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandbox execution

> Isolated command execution using Cloudflare Browser Rendering Sandboxes

## Overview

Duet provides two execution environments: the shared terminal workspace and isolated Cloudflare Sandboxes. Sandboxes allow you to run commands in a secure, ephemeral container without affecting your main workspace.

## Why sandboxes?

Sandboxes are useful for:

<CardGroup cols={2}>
  <Card title="Testing dangerous commands" icon="flask">
    Try commands like `rm -rf` without risking your workspace
  </Card>

  <Card title="AI command execution" icon="robot">
    Let the AI run commands in isolation and show you the output
  </Card>

  <Card title="Quick experiments" icon="vial">
    Test shell scripts or one-liners without cluttering your workspace
  </Card>

  <Card title="Parallel execution" icon="arrows-split-up-and-left">
    Run commands concurrently while continuing work in the main terminal
  </Card>
</CardGroup>

## Running commands in a sandbox

Press `Ctrl+R` to execute a command in the sandbox:

<Steps>
  <Step title="Open sandbox input">
    Press `Ctrl+R` in the terminal. You'll see:

    ```
    Command to run... |
    ```
  </Step>

  <Step title="Enter your command">
    Type any shell command:

    ```bash theme={null}
    ls -la && whoami
    ```
  </Step>

  <Step title="View the result">
    A toast notification appears with the output:

    ```
    $ ls -la && whoami → total 8 -rw-r--r-- 1 nobody nogroup...
    ```

    Press `Esc` to cancel without executing.
  </Step>
</Steps>

<Info>
  Sandbox execution requires a Cloudflare Worker URL, just like the AI assistant:

  ```bash theme={null}
  duet --worker https://duet-cf-worker.your-subdomain.workers.dev
  ```
</Info>

## Architecture

Sandboxes are powered by Cloudflare's Browser Rendering service:

```typescript theme={null}
import { getSandbox } from "@cloudflare/sandbox";

private async handleSandboxExec(
  roomId: string,
  rawBody: unknown
): Promise<Response> {
  const sandboxName = `sandbox-${roomId}`;

  try {
    const sandbox = getSandbox(this.env.Sandbox, sandboxName);
    const result = await sandbox.exec(data.cmd);

    return Response.json({ result, sandboxName });
  } catch (error) {
    return Response.json(
      { error: `sandbox execution failed: ${error.message}` },
      { status: 500 }
    );
  }
}
```

### Per-room sandboxes

Each room gets its own persistent sandbox instance:

```typescript theme={null}
const sandboxName = `sandbox-${roomId}`;
const sandbox = getSandbox(this.env.Sandbox, sandboxName);
```

This means:

* Files created in the sandbox persist across commands (within the same session)
* Each room's sandbox is completely isolated from others
* When the room ends, the sandbox is destroyed

<Tabs>
  <Tab title="Lifecycle">
    ```mermaid theme={null}
    stateDiagram-v2
        [*] --> Created: First command in room
        Created --> Running: Execute commands
        Running --> Running: More commands
        Running --> Destroyed: Room ends
        Destroyed --> [*]: Cleanup complete
    ```
  </Tab>

  <Tab title="Isolation">
    Sandboxes are completely isolated:

    * No access to your shared terminal workspace
    * No network access to external services
    * Separate filesystem for each room
    * Limited CPU and memory resources
  </Tab>
</Tabs>

## Command execution flow

<Steps>
  <Step title="Client sends request">
    The Go client makes an HTTP POST to the Worker:

    ```go theme={null}
    func (c *Client) ExecCommand(ctx context.Context, roomID, cmd string) (*ExecResponse, error) {
        url := fmt.Sprintf("%s/api/rooms/%s/sandbox/exec", c.baseURL, roomID)

        body := ExecRequest{
            Cmd: cmd,
        }

        jsonBody, err := json.Marshal(body)
        req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(jsonBody))
        req.Header.Set("Content-Type", "application/json")

        resp, err := c.http.Do(req)
        // ... handle response
    }
    ```
  </Step>

  <Step title="Worker validates input">
    The Worker uses Zod for schema validation:

    ```typescript theme={null}
    const SandboxExecRequestSchema = z.object({
      cmd: z.string().min(1, "Command cannot be empty"),
    });

    const parseResult = SandboxExecRequestSchema.safeParse(rawBody);
    if (!parseResult.success) {
      return Response.json(
        {
          error: "invalid request",
          details: z.flattenError(parseResult.error).fieldErrors,
        },
        { status: 400 }
      );
    }
    ```
  </Step>

  <Step title="Sandbox executes command">
    Cloudflare runs the command in an isolated container and captures stdout/stderr.
  </Step>

  <Step title="Result returned to client">
    ```json theme={null}
    {
      "result": {
        "stdout": "total 8\ndrwxr-xr-x 2 nobody nogroup 4096...",
        "stderr": ""
      },
      "sandboxName": "sandbox-a3f8e9d2-4c1b-4f3a-9e2b-8d7c6b5a4e3f"
    }
    ```
  </Step>
</Steps>

## Response format

Sandbox execution returns both stdout and stderr:

```go theme={null}
type ExecResult struct {
    Stdout string `json:"stdout"`
    Stderr string `json:"stderr"`
}

type ExecResponse struct {
    Result      ExecResult `json:"result"`
    SandboxName string     `json:"sandboxName"`
    Error       string     `json:"error,omitempty"`
}
```

The client displays whichever is available:

```go theme={null}
output := resp.Result.Stdout
if output == "" {
    output = resp.Result.Stderr
}

return SandboxResultMsg{Output: output, Cmd: cmd}
```

## AI integration

The AI assistant automatically uses sandboxes when it includes `<run>` tags:

```typescript theme={null}
private async executeCommands(text: string, roomId: string): Promise<string> {
  const matches = Array.from(text.matchAll(/<run>([\s\S]*?)<\/run>/g));
  let result = text;

  for (const match of matches) {
    const cmd = match[1]?.trim();
    if (!cmd) continue;

    try {
      const sandbox = getSandbox(this.env.Sandbox, `sandbox-${roomId}`);
      const { stderr, stdout } = await sandbox.exec(cmd);

      const summary = stdout.slice(0, 500) || stderr.slice(0, 500) || "[no output]";
      result += `\n\nOutput (${cmd}):\n${summary}`;
    } catch (e) {
      result += `\n\nError (${cmd}):\n${e.message}`;
    }
  }
  return result.replace(/<run>[\s\S]*?<\/run>/g, "").trim();
}
```

### Example

When you ask the AI:

```
You: Create a file called hello.txt with "Hello world"
```

The AI responds:

```
AI: I'll create the file for you:

<run>echo "Hello world" > hello.txt</run>

Output (echo "Hello world" > hello.txt):
[no output]
```

The command runs in the sandbox, and you can verify it worked:

```
You: Show me the contents of hello.txt

AI: <run>cat hello.txt</run>

Output (cat hello.txt):
Hello world
```

<Warning>
  Files created in the sandbox are NOT accessible from your shared terminal. Sandboxes and the terminal workspace are completely separate environments.
</Warning>

## API endpoint

<ParamField path="POST /api/rooms/:roomId/sandbox/exec" type="endpoint">
  Execute a command in the room's sandbox

  **Request body:**

  ```json theme={null}
  {
    "cmd": "ls -la && whoami"
  }
  ```

  **Success response:**

  ```json theme={null}
  {
    "result": {
      "stdout": "total 8\ndrwxr-xr-x 2 nobody nogroup 4096...\nnobody",
      "stderr": ""
    },
    "sandboxName": "sandbox-a3f8e9d2-4c1b-4f3a-9e2b-8d7c6b5a4e3f"
  }
  ```

  **Error response:**

  ```json theme={null}
  {
    "error": "sandbox execution failed: command not found"
  }
  ```
</ParamField>

## Cleanup

When a room ends, the sandbox is automatically destroyed:

```typescript theme={null}
private async handleCleanup(roomId: string): Promise<Response> {
  const errors: string[] = [];

  // Reset agent state
  this.setState({ messages: [] });

  // Terminate sandbox
  try {
    const sandbox = getSandbox(this.env.Sandbox, `sandbox-${roomId}`);
    await sandbox.destroy();
  } catch (e) {
    errors.push(`sandbox: ${e.message}`);
  }

  if (errors.length > 0) {
    return Response.json({ cleaned: true, errors }, { status: 207 });
  }

  return Response.json({ cleaned: true, roomId });
}
```

This happens when:

1. The last participant leaves the room
2. The Go server calls `DELETE /api/rooms/:roomId`
3. The Worker destroys the sandbox and clears AI state

<Info>
  Cleanup is best-effort. If the Worker is unreachable, Cloudflare will eventually garbage-collect idle sandboxes.
</Info>

## Limitations

<AccordionGroup>
  <Accordion title="No long-running processes">
    Sandboxes are designed for short commands. Long-running processes may be terminated:

    ```bash theme={null}
    # This will likely fail
    sleep 3600
    ```
  </Accordion>

  <Accordion title="Limited filesystem">
    Sandboxes have a restricted filesystem with minimal tools. Advanced utilities may not be available.
  </Accordion>

  <Accordion title="No network access">
    Sandboxes cannot make outbound network requests:

    ```bash theme={null}
    # This will fail
    curl https://example.com
    ```
  </Accordion>

  <Accordion title="Resource limits">
    CPU and memory are constrained. Intensive operations may be throttled or killed.
  </Accordion>
</AccordionGroup>

## Error handling

<Tabs>
  <Tab title="Command not found">
    ```json theme={null}
    {
      "error": "sandbox execution failed: command not found: invalid_command"
    }
    ```

    The command doesn't exist in the sandbox environment.
  </Tab>

  <Tab title="Execution timeout">
    ```json theme={null}
    {
      "error": "sandbox execution failed: timeout"
    }
    ```

    The command took too long to execute.
  </Tab>

  <Tab title="Validation error">
    ```json theme={null}
    {
      "error": "invalid request",
      "details": {
        "cmd": ["Command cannot be empty"]
      }
    }
    ```

    Empty commands are rejected.
  </Tab>
</Tabs>

## Comparison: Sandbox vs Terminal

| Feature         | Shared Terminal                    | Sandbox                 |
| --------------- | ---------------------------------- | ----------------------- |
| **Persistence** | Permanent (until room ends)        | Ephemeral (per-room)    |
| **Visibility**  | All participants see output        | Only command initiator  |
| **Filesystem**  | Shared workspace                   | Isolated per room       |
| **Tools**       | Full shell with installed packages | Minimal environment     |
| **Network**     | Full access                        | No outbound connections |
| **Use case**    | Primary development work           | Testing, AI experiments |

## Best practices

<CardGroup cols={2}>
  <Card title="Use for experiments" icon="flask">
    Test unfamiliar commands in the sandbox before running in the terminal
  </Card>

  <Card title="Check output length" icon="ruler">
    Sandbox output is truncated to 500 characters. For long output, use the terminal.
  </Card>

  <Card title="Don't rely on state" icon="warning">
    Sandboxes are destroyed when the room ends. Use the terminal for persistent work.
  </Card>

  <Card title="Verify AI commands" icon="magnifying-glass">
    Always review AI-generated commands before manually running them in your terminal.
  </Card>
</CardGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="AI assistant" icon="sparkles" href="/features/ai-assistant">
    Learn how the AI uses sandboxes for command execution
  </Card>

  <Card title="Deploy a Worker" icon="cloud" href="/deployment/cloudflare-worker">
    Set up Cloudflare Worker and Sandbox bindings
  </Card>
</CardGroup>
